<template>
  <view class="uni-file-picker">
    <view v-if="title" class="uni-file-picker__header">
      <text class="file-title">{{ title }}</text>
      <text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
    </view>
    <upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly"
                  :image-styles="imageStyles" :files-list="filesList" :limit="limitLength"
                  :disablePreview="disablePreview"
                  :delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
      <slot>
        <view class="is-add">
          <view class="icon-add"></view>
          <view class="icon-add rotate"></view>
        </view>
      </slot>
    </upload-image>
    <upload-file v-if="fileMediatype !== 'image' || showType !== 'grid'" :readonly="readonly"
                 :list-styles="listStyles" :files-list="filesList" :showType="showType" :delIcon="delIcon"
                 @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
      <slot>
        <button type="primary" size="mini">选择文件</button>
      </slot>
    </upload-file>
  </view>
</template>

<script>
import {
  chooseAndUploadFile,
  uploadCloudFiles
} from './choose-and-upload-file.js'
import {
  get_file_ext,
  get_extname,
  get_files_and_is_max,
  get_file_info,
  get_file_data
} from './utils.js'
import uploadImage from './upload-image.vue'
import uploadFile from './upload-file.vue'

let fileInput = null
/**
 * FilePicker 文件选择上传
 * @description 文件选择上传组件，可以选择图片、视频等任意文件并上传到当前绑定的服务空间
 * @tutorial https://ext.dcloud.net.cn/plugin?id=4079
 * @property {Object|Array}  value  组件数据，通常用来回显 ,类型由return-type属性决定
 * @property {Boolean}  disabled = [true|false]  组件禁用
 *  @value true  禁用
 *  @value false  取消禁用
 * @property {Boolean}  readonly = [true|false]  组件只读，不可选择，不显示进度，不显示删除按钮
 *  @value true  只读
 *  @value false  取消只读
 * @property {String}  return-type = [array|object]  限制 value 格式，当为 object 时 ，组件只能单选，且会覆盖
 *  @value array  规定 value 属性的类型为数组
 *  @value object  规定 value 属性的类型为对象
 * @property {Boolean}  disable-preview = [true|false]  禁用图片预览，仅 mode:grid 时生效
 *  @value true  禁用图片预览
 *  @value false  取消禁用图片预览
 * @property {Boolean}  del-icon = [true|false]  是否显示删除按钮
 *  @value true  显示删除按钮
 *  @value false  不显示删除按钮
 * @property {Boolean}  auto-upload = [true|false]  是否自动上传，值为true则只触发@select,可自行上传
 *  @value true  自动上传
 *  @value false  取消自动上传
 * @property {Number|String}  limit  最大选择个数 ，h5 会自动忽略多选的部分
 * @property {String}  title  组件标题，右侧显示上传计数
 * @property {String}  mode = [list|grid]  选择文件后的文件列表样式
 *  @value list  列表显示
 *  @value grid  宫格显示
 * @property {String}  file-mediatype = [image|video|all]  选择文件类型
 *  @value image  只选择图片
 *  @value video  只选择视频
 *  @value all    选择所有文件
 * @property {Array}  file-extname  选择文件后缀，根据 file-mediatype 属性而不同
 * @property {Object}  list-style  mode:list 时的样式
 * @property {Object}  image-styles  选择文件后缀，根据 file-mediatype 属性而不同
 * @event {Function} select  选择文件后触发
 * @event {Function} progress 文件上传时触发
 * @event {Function} success  上传成功触发
 * @event {Function} fail    上传失败触发
 * @event {Function} delete  文件从列表移除时触发
 */
export default {
  name: 'uniFilePicker',
  components: {
    uploadImage,
    uploadFile
  },
  options: {
    virtualHost: true
  },
  emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'input'],
  props: {
    // #ifdef VUE3
    modelValue: {
      type: [Array, Object],
      default() {
        return []
      }
    },
    // #endif

    // #ifndef VUE3
    value: {
      type: [Array, Object],
      default() {
        return []
      }
    },
    // #endif

    disabled: {
      type: Boolean,
      default: false
    },
    disablePreview: {
      type: Boolean,
      default: false
    },
    delIcon: {
      type: Boolean,
      default: true
    },
    // 自动上传
    autoUpload: {
      type: Boolean,
      default: true
    },
    // 最大选择个数 ，h5只能限制单选或是多选
    limit: {
      type: [Number, String],
      default: 9
    },
    // 列表样式 grid | list | list-card
    mode: {
      type: String,
      default: 'grid'
    },
    // 选择文件类型  image/video/all
    fileMediatype: {
      type: String,
      default: 'image'
    },
    // 文件类型筛选
    fileExtname: {
      type: [Array, String],
      default() {
        return []
      }
    },
    title: {
      type: String,
      default: ''
    },
    listStyles: {
      type: Object,
      default() {
        return {
          // 是否显示边框
          border: true,
          // 是否显示分隔线
          dividline: true,
          // 线条样式
          borderStyle: {}
        }
      }
    },
    imageStyles: {
      type: Object,
      default() {
        return {
          width: 'auto',
          height: 'auto'
        }
      }
    },
    readonly: {
      type: Boolean,
      default: false
    },
    returnType: {
      type: String,
      default: 'array'
    },
    sizeType: {
      type: Array,
      default() {
        return ['original', 'compressed']
      }
    },
    sourceType: {
      type: Array,
      default() {
        return ['album', 'camera']
      }
    },
    provider: {
      type: String,
      default: '' // 默认上传到 unicloud 内置存储 extStorage 扩展存储
    }
  },
  data() {
    return {
      files: [],
      localValue: []
    }
  },
  watch: {
    // #ifndef VUE3
    value: {
      handler(newVal, oldVal) {
        this.setValue(newVal, oldVal)
      },
      immediate: true
    },
    // #endif
    // #ifdef VUE3
    modelValue: {
      handler(newVal, oldVal) {
        this.setValue(newVal, oldVal)
      },
      immediate: true
    },
    // #endif
  },
  computed: {
    filesList() {
      let files = []
      this.files.forEach(v => {
        files.push(v)
      })
      return files
    },
    showType() {
      if (this.fileMediatype === 'image') {
        return this.mode
      }
      return 'list'
    },
    limitLength() {
      if (this.returnType === 'object') {
        return 1
      }
      if (!this.limit) {
        return 1
      }
      if (this.limit >= 9) {
        return 9
      }
      return this.limit
    }
  },
  created() {
    // TODO 兼容不开通服务空间的情况
    if (!(uniCloud.config && uniCloud.config.provider)) {
      this.noSpace = true
      uniCloud.chooseAndUploadFile = chooseAndUploadFile
    }
    this.form = this.getForm('uniForms')
    this.formItem = this.getForm('uniFormsItem')
    if (this.form && this.formItem) {
      if (this.formItem.name) {
        this.rename = this.formItem.name
        this.form.inputChildrens.push(this)
      }
    }
  },
  methods: {
    /**
     * 公开用户使用，清空文件
     * @param {Object} index
     */
    clearFiles(index) {
      if (index !== 0 && !index) {
        this.files = []
        this.$nextTick(() => {
          this.setEmit()
        })
      } else {
        this.files.splice(index, 1)
      }
      this.$nextTick(() => {
        this.setEmit()
      })
    },
    /**
     * 公开用户使用，继续上传
     */
    upload() {
      let files = []
      this.files.forEach((v, index) => {
        if (v.status === 'ready' || v.status === 'error') {
          files.push(Object.assign({}, v))
        }
      })
      return this.uploadFiles(files)
    },
    async setValue(newVal, oldVal) {
      const newData = async (v) => {
        const reg = /cloud:\/\/([\w.]+\/?)\S*/
        let url = ''
        if (v.fileID) {
          url = v.fileID
        } else {
          url = v.url
        }
        if (reg.test(url)) {
          v.fileID = url
          v.url = await this.getTempFileURL(url)
        }
        if (v.url) v.path = v.url
        return v
      }
      if (this.returnType === 'object') {
        if (newVal) {
          await newData(newVal)
        } else {
          newVal = {}
        }
      } else {
        if (!newVal) newVal = []
        for (let i = 0; i < newVal.length; i++) {
          let v = newVal[i]
          await newData(v)
        }
      }
      this.localValue = newVal
      if (this.form && this.formItem && !this.is_reset) {
        this.is_reset = false
        this.formItem.setValue(this.localValue)
      }
      let filesData = Object.keys(newVal).length > 0 ? newVal : [];
      this.files = [].concat(filesData)
    },

    /**
     * 选择文件
     */
    choose() {
      if (this.disabled) return
      if (this.files.length >= Number(this.limitLength) && this.showType !== 'grid' && this.returnType ===
          'array') {
        uni.showToast({
          title: `您最多选择 ${this.limitLength} 个文件`,
          icon: 'none'
        })
        return
      }
      this.chooseFiles()
    },

    /**
     * 选择文件并上传
     */
    chooseFiles() {
      const _extname = get_extname(this.fileExtname)
      // 获取后缀
      uniCloud
          .chooseAndUploadFile({
            type: this.fileMediatype,
            compressed: false,
            sizeType: this.sizeType,
            sourceType: this.sourceType,
            // TODO 如果为空，video 有问题
            extension: _extname.length > 0 ? _extname : undefined,
            count: this.limitLength - this.files.length, //默认9
            onChooseFile: this.chooseFileCallback,
            onUploadProgress: progressEvent => {
              this.setProgress(progressEvent, progressEvent.index)
            }
          })
          .then(result => {
            this.setSuccessAndError(result.tempFiles)
          })
          .catch(err => {
            console.log('选择失败', err)
          })
    },

    /**
     * 选择文件回调
     * @param {Object} res
     */
    async chooseFileCallback(res) {
      const _extname = get_extname(this.fileExtname)
      const is_one = (Number(this.limitLength) === 1 &&
              this.disablePreview &&
              !this.disabled) ||
          this.returnType === 'object'
      // 如果这有一个文件 ，需要清空本地缓存数据
      if (is_one) {
        this.files = []
      }

      let {
        filePaths,
        files
      } = get_files_and_is_max(res, _extname)
      if (!(_extname && _extname.length > 0)) {
        filePaths = res.tempFilePaths
        files = res.tempFiles
      }

      let currentData = []
      for (let i = 0; i < files.length; i++) {
        if (this.limitLength - this.files.length <= 0) break
        files[i].uuid = Date.now()
        let filedata = await get_file_data(files[i], this.fileMediatype)
        filedata.progress = 0
        filedata.status = 'ready'
        this.files.push(filedata)
        currentData.push({
          ...filedata,
          file: files[i]
        })
      }
      this.$emit('select', {
        tempFiles: currentData,
        tempFilePaths: filePaths
      })
      res.tempFiles = files
      // 停止自动上传
      if (!this.autoUpload || this.noSpace) {
        res.tempFiles = []
      }
      res.tempFiles.forEach((fileItem, index) => {
        this.provider && (fileItem.provider = this.provider);
        const fileNameSplit = fileItem.name.split('.')
        const ext = fileNameSplit.pop()
        const fileName = fileNameSplit.join('.').replace(/[\s\/\?<>\\:\*\|":]/g, '_')
        fileItem.cloudPath = fileName + '_' + Date.now() + '_' + index + '.' + ext
      })
    },

    /**
     * 批传
     * @param {Object} e
     */
    uploadFiles(files) {
      files = [].concat(files)
      return uploadCloudFiles.call(this, files, 5, res => {
        this.setProgress(res, res.index, true)
      })
          .then(result => {
            this.setSuccessAndError(result)
            return result;
          })
          .catch(err => {
            console.log(err)
          })
    },

    /**
     * 成功或失败
     */
    async setSuccessAndError(res, fn) {
      let successData = []
      let errorData = []
      let tempFilePath = []
      let errorTempFilePath = []
      for (let i = 0; i < res.length; i++) {
        const item = res[i]
        const index = item.uuid ? this.files.findIndex(p => p.uuid === item.uuid) : item.index

        if (index === -1 || !this.files) break
        if (item.errMsg === 'request:fail') {
          this.files[index].url = item.path
          this.files[index].status = 'error'
          this.files[index].errMsg = item.errMsg
          // this.files[index].progress = -1
          errorData.push(this.files[index])
          errorTempFilePath.push(this.files[index].url)
        } else {
          this.files[index].errMsg = ''
          this.files[index].fileID = item.url
          const reg = /cloud:\/\/([\w.]+\/?)\S*/
          if (reg.test(item.url)) {
            this.files[index].url = await this.getTempFileURL(item.url)
          } else {
            this.files[index].url = item.url
          }

          this.files[index].status = 'success'
          this.files[index].progress += 1
          successData.push(this.files[index])
          tempFilePath.push(this.files[index].fileID)
        }
      }

      if (successData.length > 0) {
        this.setEmit()
        // 状态改变返回
        this.$emit('success', {
          tempFiles: this.backObject(successData),
          tempFilePaths: tempFilePath
        })
      }

      if (errorData.length > 0) {
        this.$emit('fail', {
          tempFiles: this.backObject(errorData),
          tempFilePaths: errorTempFilePath
        })
      }
    },

    /**
     * 获取进度
     * @param {Object} progressEvent
     * @param {Object} index
     * @param {Object} type
     */
    setProgress(progressEvent, index, type) {
      const fileLenth = this.files.length
      const percentNum = (index / fileLenth) * 100
      const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
      let idx = index
      if (!type) {
        idx = this.files.findIndex(p => p.uuid === progressEvent.tempFile.uuid)
      }
      if (idx === -1 || !this.files[idx]) return
      // fix by mehaotian 100 就会消失，-1 是为了让进度条消失
      this.files[idx].progress = percentCompleted - 1
      // 上传中
      this.$emit('progress', {
        index: idx,
        progress: parseInt(percentCompleted),
        tempFile: this.files[idx]
      })
    },

    /**
     * 删除文件
     * @param {Object} index
     */
    delFile(index) {
      this.$emit('delete', {
        tempFile: this.files[index],
        tempFilePath: this.files[index].url
      })
      this.files.splice(index, 1)
      this.$nextTick(() => {
        this.setEmit()
      })
    },

    /**
     * 获取文件名和后缀
     * @param {Object} name
     */
    getFileExt(name) {
      const last_len = name.lastIndexOf('.')
      const len = name.length
      return {
        name: name.substring(0, last_len),
        ext: name.substring(last_len + 1, len)
      }
    },

    /**
     * 处理返回事件
     */
    setEmit() {
      let data = []
      if (this.returnType === 'object') {
        data = this.backObject(this.files)[0]
        this.localValue = data ? data : null
      } else {
        data = this.backObject(this.files)
        if (!this.localValue) {
          this.localValue = []
        }
        this.localValue = [...data]
      }
      // #ifdef VUE3
      this.$emit('update:modelValue', this.localValue)
      // #endif
      // #ifndef VUE3
      this.$emit('input', this.localValue)
      // #endif
    },

    /**
     * 处理返回参数
     * @param {Object} files
     */
    backObject(files) {
      let newFilesData = []
      files.forEach(v => {
        newFilesData.push({
          extname: v.extname,
          fileType: v.fileType,
          image: v.image,
          name: v.name,
          path: v.path,
          size: v.size,
          fileID: v.fileID,
          url: v.url,
          // 修改删除一个文件后不能再上传的bug, #694
          uuid: v.uuid,
          status: v.status,
          cloudPath: v.cloudPath
        })
      })
      return newFilesData
    },
    async getTempFileURL(fileList) {
      fileList = {
        fileList: [].concat(fileList)
      }
      const urls = await uniCloud.getTempFileURL(fileList)
      return urls.fileList[0].tempFileURL || ''
    },
    /**
     * 获取父元素实例
     */
    getForm(name = 'uniForms') {
      let parent = this.$parent;
      let parentName = parent.$options.name;
      while (parentName !== name) {
        parent = parent.$parent;
        if (!parent) return false;
        parentName = parent.$options.name;
      }
      return parent;
    }
  }
}
</script>

<style>
.uni-file-picker {
  /* #ifndef APP-NVUE */
  box-sizing: border-box;
  overflow: hidden;
  width: 100%;
  /* #endif */
  flex: 1;
}

.uni-file-picker__header {
  padding-top: 5px;
  padding-bottom: 10px;
  /* #ifndef APP-NVUE */
  display: flex;
  /* #endif */
  justify-content: space-between;
}

.file-title {
  font-size: 14px;
  color: #333;
}

.file-count {
  font-size: 14px;
  color: #999;
}

.is-add {
  /* #ifndef APP-NVUE */
  display: flex;
  /* #endif */
  align-items: center;
  justify-content: center;
}

.icon-add {
  width: 50px;
  height: 5px;
  background-color: #f1f1f1;
  border-radius: 2px;
}

.rotate {
  position: absolute;
  transform: rotate(90deg);
}
</style>
